export type CallBack<T, R> = (item: T, parent: T | null, index: number) => R

export const otherFields = {
  parent: Symbol('parent') // 转list的时候记录父子关系
}

export class TreeUtils<T> {
  private readonly tree: T[]
  private readonly children: string

  constructor(tree: T[], children = 'children') {
    this.tree = tree
    this.children = children
  }

  getResult() {
    return this.tree
  }

  forEach(callBack: CallBack<T, void>): TreeUtils<T> {
    TreeUtils.treeForEach(this.tree, callBack, this.children)
    return new TreeUtils(this.tree, this.children)
  }

  map<R>(callBack: CallBack<T, R>): TreeUtils<R> {
    const result = TreeUtils.treeMap(this.tree, callBack, this.children)
    return new TreeUtils(result, this.children)
  }

  filter(callBack: CallBack<T, boolean>): TreeUtils<T> {
    const result = TreeUtils.treeFilter(this.tree, callBack, this.children)
    return new TreeUtils(result, this.children)
  }

  search(callBack: CallBack<T, boolean>): TreeUtils<T> {
    const result = TreeUtils.treeSearch(this.tree, callBack, this.children)
    return new TreeUtils(result, this.children)
  }

  find(callBack: CallBack<T, boolean>): T | null {
    return TreeUtils.treeFind(this.tree, callBack, this.children)
  }

  static treeForEach<T>(tree: T[], callBack: CallBack<T, void>, children = 'children'): void {
    TreeUtils.treeForEachFindParent(tree, null, callBack, children)
  }

  private static treeForEachFindParent<T>(tree: T[], parent: T | null, callBack: CallBack<T, void>, children = 'children'): void {
    tree.forEach((i, index) => {
      callBack(i, parent, index)
      if (i[children]) {
        TreeUtils.treeForEachFindParent(i[children], i, callBack, children)
      }
    })
  }

  static treeMap<T, R>(tree: T[], callBack: CallBack<T, R>, children = 'children'): R[] {
    return TreeUtils.treeMapFindParent(tree, null, callBack, children)
  }

  private static treeMapFindParent<T, R>(tree: T[], parent: T | null, callBack: CallBack<T, R>, children = 'children'): R[] {
    const result: R[] = []
    tree.forEach((i, index) => {
      const val = callBack(i, parent, index)
      if (i[children]) {
        val[children] = TreeUtils.treeMapFindParent(i[children], i, callBack, children)
      }
      result.push(val)
    })
    return result
  }

  static treeFind<T>(tree: T[], callBack: CallBack<T, boolean>, children = 'children'): T | null {
    return TreeUtils.treeFindFindParent(tree, null, callBack, children)
  }


  private static treeFindFindParent<T>(tree: T[], parent: T | null, callBack: CallBack<T, boolean>, children = 'children'): T | null {
    for (let index = 0; index < tree.length; index++) {
      const i = tree[index]
      const val = callBack(i, parent, index)
      if (val) {
        return i
      }
      if (i[children]) {
        const find = TreeUtils.treeFindFindParent(i[children], i, callBack, children)
        if (find) {
          return find
        }
      }
    }
    return null
  }

  /**
   * 不会保留子项
   * 要保留子项请用treeSearch
   * @param tree
   * @param callBack
   * @param children
   */
  static treeFilter<T>(tree: T[], callBack: CallBack<T, boolean>, children = 'children'): T[] {
    return TreeUtils.treeFilterFindParent(tree, null, callBack, children)
  }

  private static treeFilterFindParent<T>(tree: T[], parent: T | null, callBack: CallBack<T, boolean>, children = 'children'): T[] {
    return tree.filter((item, index) => {
      const val = callBack(item, parent, index)
      if (val) {
        if (item[children]) {
          item[children] = TreeUtils.treeFilterFindParent(item[children], item, callBack, children)
        }
        return true
      }
      return false
    })
  }


  /**
   * 过滤保留子项
   * 如果过滤不保留子项请用treeFilter
   * @param tree
   * @param callBack
   * @param children
   */
  static treeSearch<T>(tree: T[], callBack: CallBack<T, boolean>, children = 'children'): T[] {
    return TreeUtils.treeSearchFindParent(tree, null, callBack, children)
  }

  private static treeSearchFindParent<T>(tree: T[], parent: T | null, callBack: CallBack<T, boolean>, children = 'children'): T[] {
    return tree.filter((item, index) => {
      if (item[children]) {
        item[children] = TreeUtils.treeSearchFindParent(item[children], item, callBack, children)
        if (item[children].length) {
          return true
        }
      }
      return callBack(item, parent, index)
    })
  }

  toList(recordParent = true): T[] {
    return TreeUtils.toList(this.tree, this.children, recordParent)
  }

  public static toList<T>(tree: T[], children = 'children', recordParent = true): T[] {
    const result: T[] = []
    TreeUtils.treeForEach(tree, (item, parent, index) => {
      if (recordParent) {
        if (parent) {
          item[otherFields.parent] = parent[otherFields.parent] + '-' + index
        } else {
          item[otherFields.parent] = '' + index
        }
      }
      result.push(item)
    })
    return result
  }
}
